-- Copyright (c) 2011-2012 Casey Baxter
-- See LICENSE file for details

---------------------------------------------------------------------------------------------------
-- -= Map =-
---------------------------------------------------------------------------------------------------
-- Setup

-- Import the other classes
local PATH = (...):gsub("[\\/]", ""):match(".+%.") or ''

--local Tile        = require(PATH .. "Tile")
local TileSet     = require(PATH .. "TileSet")
local TileLayer   = require(PATH .. "TileLayer")
local Object      = require(PATH .. "Object")
local ObjectLayer = require(PATH .. "ObjectLayer")

local ceil = math.ceil
local floor = math.floor

-- Make our map class
local Map = {class = "Map"}
Map.__index = Map

---------------------------------------------------------------------------------------------------
-- Returns a new map
function Map.init(o)
    local map = setmetatable(o, Map)
    
    map.viewX, map.viewY = 0, 0
    map.viewW, map.viewH = love.graphics.getDimensions()
    map.viewScaling = 1
    map.viewPadding = 10
    
    map.offsetX, map.offsetY = 0, 0
    
    --map.layers  = {}            -- Layers of the map indexed by name
    --map.tilesets = {}           -- Tilesets indexed by name
    --map.tiles = {}              -- Tiles indexed by id
    --map.layerOrder = {}         -- The order of the layers. Callbacks are called in this order.

    -- Private:
    map._widestTile = 0                 -- The widest tile on the map.
    map._highestTile = 0                -- The tallest tile on the map.
    map._forceRedraw = false            -- If true then the next redraw is forced
    map._previousUseSpriteBatch = false -- The previous useSpiteBatch.
    map._tileClipboard  =   nil         -- The value that stored for tile copying and pasting.
    
    -- Return the new map
    return map
end

---------------------------------------------------------------------------------------------------
-- Creates a new tileset and adds it to the map. The map will then auto-update its tiles.
--function Map:newTileSet(...)
--    local tileset = TileSet:new(...)
--    local name = tileset.name
--    if self.tilesets[name] then 
--        error(  string.format("Map:newTileSet - A tile set named \"%s\" already exists.", name) )
--    end
--    self.tilesets[name] = TileSet:new(...)
--    self:updateTiles()
--    return tileset
--end

---------------------------------------------------------------------------------------------------
-- Creates a new TileLayer and adds it to the map. The position parameter is the position to insert
-- the layer into the layerOrder.
--function Map:newTileLayer(position, ...)
--    local layer = TileLayer:new(self, ...)
--    local name = layer.name
--    if self.layers[name] then 
--        error( string.format("Map:newTileLayer - A layer named \"%s\" already exists.", name) )
--    end
--    self.layers[name] = layer
--    table.insert(self.layerOrder, position or #self.layerOrder + 1, layer)
--    return layer
--end

---------------------------------------------------------------------------------------------------
-- Creates a new ObjectLayer and inserts it into the map
--function Map:newObjectLayer(position, ...)
--    local layer = ObjectLayer:new(self, ...)
--    local name = layer.name
--    if self.layers[name] then 
--        error( string.format("Map:newObjectLayer - A layer named \"%s\" already exists.", name) )
--    end
--    self.layers[name] = layer
--    table.insert(self.layerOrder, position or #self.layerOrder + 1, layer)
--    return layer
--end

---------------------------------------------------------------------------------------------------
-- Add a custom layer to the map. You can include a predefined layer table or one will be created.
function Map:newCustomLayer(name, position, layer)
    if self.layers[name] then 
        error( string.format("Map:newCustomLayer - The layer name \"%s\" already exists.", name) )
    end
    self.layers[name] = layer or {name=name}
    self.layers[name].class = "CustomLayer"
    table.insert(self.layerOrder, position or #self.layerOrder + 1, self.layers[name])
    return self.layers[name]
end

---------------------------------------------------------------------------------------------------
-- Cuts tiles out of tilesets and stores them in the tiles tables under their id
-- Call this after the tilesets are set up
function Map:updateTiles()
    self.tiles = {}
    self._widestTile = 0
    self._highestTile = 0
    for _, ts in pairs(self.tilesets) do
        if ts.tileWidth > self._widestTile then self._widestTile = ts.tileWidth end
        if ts.tileHeight > self._highestTile then self._highestTile = ts.tileHeight end
        for id, val in pairs(ts:getTiles()) do
            self.tiles[id] = val
        end
    end
end

---------------------------------------------------------------------------------------------------
-- Forces the map to redraw the sprite batches.
function Map:forceRedraw()
    self._forceRedraw = true
end

---------------------------------------------------------------------------------------------------
-- Performs a callback on all map layers.
--local layer
--function Map:callback(cb, ...)
--    if cb == "draw" then self:_updateTileRange() end
--    for i = 1, #self.layerOrder do
--        layer = self.layerOrder[i]
--        if layer[cb] then layer[cb](layer, ...) end
--    end
--end

---------------------------------------------------------------------------------------------------
-- Draw the map.
function Map:draw()
	if not self.visible then return end
	
	self:_updateTileRange()
	for i, v in ipairs(self.layerOrder) do
		if v.draw then v.draw(v) end
	end
end

---------------------------------------------------------------------------------------------------
-- returns layer's index inside layerOrder (or nil).
-- layer can be layerObject or string
function Map:layerPosition(layer)
	if type(layer) == "string" then layer = self.layers[layer] end
	if not (type(layer) == "table" and layer.class) then return end
	for i, v in ipairs(self.layerOrder) do
		if v == layer then return i end
	end
end

---------------------------------------------------------------------------------------------------
-- layer1-2 can be layerObject, number or string
function Map:swapLayers(layer1, layer2)
	if type(layer1) ~= "number" then layer1 = self:layerPosition(layer1) end
	if type(layer2) ~= "number" then layer2 = self:layerPosition(layer2) end
	local lo = self.layerOrder
	local l1, l2 = lo[layer1], lo[layer2]
	if not l1 or not l2 then return end
	lo[layer1], lo[layer2] = l2, l1
end

---------------------------------------------------------------------------------------------------
-- layer can be layerObject, number or string
function Map:removeLayer(layer)
	if type(layer) ~= "number" then layer = self:layerPosition(layer) end
	local lo = self.layerOrder
	if not lo[layer] then return end
	layer = table.remove(lo, layer)
	self.layers[layer.name] = nil
	return layer
end

---------------------------------------------------------------------------------------------------
-- Turns an isometric location into a world location. The unit length for isometric tiles is always
-- the map's tileHeight. This is both for width and height.
function Map:fromIso(x, y)
	return ((x - y) / self.tileHeight + self.height - 1) * self.tileWidth / 2, (x + y) / 2
end

---------------------------------------------------------------------------------------------------
-- Turns a world location into an isometric location
function Map:toIso(a, b)
	a, b = a or 0, b or 0
	a = b - ((self.height - 1) / 2 + a / self.tileWidth) * self.tileHeight
	b = 2 * b - a
	return a, b
end

---------------------------------------------------------------------------------------------------
-- Sets the draw range
function Map:setDrawRange(vx, vy, vw, vh, vp, vs)
	self.viewX, self.viewY, self.viewW, self.viewH = vx, vy, vw, vh
	self.viewPadding = vp or 0
	self.viewScaling = vs or 1
end

---------------------------------------------------------------------------------------------------
-- Automatically sets the draw range to fit the display
function Map:autoDrawRange(tx, ty, vs, vp)
	self.viewX = tx and -tx or 0
	self.viewY = ty and -ty or 0
	self.viewW, self.viewH = love.graphics.getDimensions()
	vs = vs and (vs > 0.001 and vs or 0.001) or 1
	self.viewScaling = vs
	self.viewPadding = vp or 0
end

---------------------------------------------------------------------------------------------------
-- Returns the normal draw range -- FIXME: where is parallax?
function Map:getDrawRange()
	local vp, vs = self.viewPadding, self.viewScaling
	return self.viewX - vp, self.viewY - vp, self.viewW/vs + vp * 2, self.viewH/vs + vp * 2
end

----------------------------------------------------------------------------------------------------
-- Private Functions
----------------------------------------------------------------------------------------------------
-- This is an internal function used to update the map's _tileRange, _previousTileRange, and 
-- _specialRedraw
local x1, y1, x2, y2, heightOffset, widthOffset, tr, ptr, layer
function Map:_updateTileRange()
    
    -- Offset to make sure we can always draw the highest and widest tile
    heightOffset = self._highestTile - self.tileHeight
    widthOffset = self._widestTile - self.tileWidth
    
    -- Go through each layer
    for i = 1,#self.layerOrder do
        layer = self.layerOrder[i]

        -- If the layer is a TileLayer
        if layer.class == "TileLayer" then
        
            -- Get the draw range. 
            x1 = self.viewX * layer.parallaxX - self.viewPadding + layer.offsetX
            y1 = self.viewY * layer.parallaxY - self.viewPadding + layer.offsetY
            x2 = self.viewW/self.viewScaling + self.viewPadding*2
            y2 = self.viewH/self.viewScaling + self.viewPadding*2
            
            -- Apply the offset
            x1 = x1 - self.offsetX - layer.offsetX
            y1 = y1 - self.offsetY - layer.offsetY
        
            -- Calculate the _tileRange for orthogonal tiles
            if self.orientation == "orthogonal" then
        
                -- Limit the drawing range. We must make sure we can draw the tiles that are bigger
                -- than the self's tileWidth and tileHeight.
                if x1 and y1 and x2 and y2 then
                    x2 = ceil(x2/self.tileWidth)
                    y2 = ceil((y2+heightOffset)/self.tileHeight)
                    x1 = floor((x1-widthOffset)/self.tileWidth)
                    y1 = floor(y1/self.tileHeight)
            
                    -- Make sure that we stay within the boundry of the map
                    x1 = x1 > 0 and x1 or 0
                    y1 = y1 > 0 and y1 or 0
                    x2 = x2 < self.width and x2 or self.width - 1
                    y2 = y2 < self.height and y2 or self.height - 1
            
                else
                    -- If the drawing range isn't defined then we draw all the tiles
                    x1, y1, x2, y2 = 0, 0, self.width-1, self.height-1
                end
            
            -- Calculate the _tileRange for isometric tiles.
            else
                -- If the drawRange is set
                if x1 and y1 and x2 and y2 then
                    x1, y1 = self:toIso(x1-self._widestTile,y1)
                    x1, y1 = ceil(x1/self.tileHeight), ceil(y1/self.tileHeight)-1
                    x2 = ceil((x2+self._widestTile)/self.tileWidth)
                    y2 = ceil((y2+heightOffset)/self.tileHeight)
                -- else draw everything
                else
                    x1 = 0
                    y1 = 0
                    x2 = self.width - 1
                    y2 = self.height - 1
                end
            end
        
            -- Assign the new values to the tile range
            tr, ptr = layer._tileRange, layer._previousTileRange
            ptr[1], ptr[2], ptr[3], ptr[4] = tr[1], tr[2], tr[3], tr[4]
            tr[1], tr[2], tr[3], tr[4] =  x1, y1, x2, y2
        
            -- If the tile range or useSpriteBatch is different than the last frame then we need to 
            -- update its sprite batches.
            layer._redraw = self.useSpriteBatch ~= self._previousUseSpriteBatch or 
                            self._forceRedraw or
                            tr[1] ~= ptr[1] or 
                            tr[2] ~= ptr[2] or 
                            tr[3] ~= ptr[3] or 
                            tr[4] ~= ptr[4]
        end
    end
    
    -- Set the previous useSpritebatch
    self._previousUseSpriteBatch = self.useSpriteBatch
                          
    -- Reset the forced special redraw
    self._forceRedraw = false
end

---------------------------------------------------------------------------------------------------
-- Calling the map as a function will return the layer
function Map:__call(layerName)
    return self.layers[layerName]
end

---------------------------------------------------------------------------------------------------
-- Returns the Map class
return Map
